﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using QuantBox.Serialization;
using QuantBox.XApi;

namespace QuantBox
{
    public class DataCenterClient
    {
        private readonly bool _enableCache;
        private readonly string _dataCachePath;
        private readonly HttpClient? _client;
        private readonly HttpClient? _tickClient;

        private static BarDataField[] ToBarData(List<string[]> items)
        {
            return items.Select(ToBarData).ToArray();
        }

        private static BarDataField ToBarData(string[] data)
        {
            var i = 0;
            DateTime closeDateTime;
            DateTime openDateTime;
            var date = data[++i];
            int barSize;
            if (date.Length == 8)
            {
                DateTime.TryParseExact(date, "yyyyMMdd", null, DateTimeStyles.None, out openDateTime);
                closeDateTime = openDateTime.AddDays(1);
                barSize = 86400;
            }
            else
            {
                DateTime.TryParseExact(date, "yyyy-MM-dd HH:mm:ss",
                    null, DateTimeStyles.None, out openDateTime);
                DateTime.TryParseExact(data[++i], "yyyy-MM-dd HH:mm:ss",
                    null, DateTimeStyles.None, out closeDateTime);
                barSize = (int)(closeDateTime - openDateTime).TotalSeconds;
            }

            double.TryParse(data[++i], out var open);
            double.TryParse(data[++i], out var high);
            double.TryParse(data[++i], out var low);
            double.TryParse(data[++i], out var close);
            double.TryParse(data[++i], out var volume);
            double.TryParse(data[++i], out var amount);
            var openInt = 0d;
            ++i;
            if (i < data.Length)
            {
                double.TryParse(data[i], out openInt);
            }

            var bar = new BarDataField
            {
                Symbol = data[0],
                BarSize = barSize,
                CloseDateTime = closeDateTime,
                OpenDateTime = openDateTime,
                Open = open,
                High = high,
                Close = close,
                Low = low,
                Volume = (int)volume,
                Turnover = amount,
                OpenInterest = (int)openInt
            };

            if (barSize == 86400)
            {
                double.TryParse(data[++i], out var settle);
                double.TryParse(data[++i], out var preSettle);
                double.TryParse(data[++i], out var preClose);
                bar.Settle = settle;
                bar.PreSettle = preSettle;
                bar.PreClose = preClose;
            }

            return bar;
        }

        private static (string, ErrorInfo?) ParseStringResponse(HttpResponseMessage? response, CancellationToken ct)
        {
            if (response == null)
            {
                return (string.Empty, null);
            }
            var task = response.Content.ReadAsStringAsync();
            task.Wait(ct);
            if (response.IsSuccessStatusCode)
            {
                return (task.Result, null);
            }
            return (string.Empty, JsonConvert.DeserializeObject<ErrorInfo>(task.Result));
        }

        private static (T, ErrorInfo?) ParseJsonResponse<T>(HttpResponseMessage? response, CancellationToken ct)
        {
            if (response == null)
            {
                return (default, null)!;
            }
            var task = response.Content.ReadAsStringAsync();
            task.Wait(ct);
            if (response.IsSuccessStatusCode)
            {
                return (JsonConvert.DeserializeObject<T>(task.Result), null);
            }
            return (default, JsonConvert.DeserializeObject<ErrorInfo>(task.Result))!;
        }

        private static (Stream?, ErrorInfo?) ParseFileResponse(HttpResponseMessage? response, CancellationToken ct)
        {
            if (response == null)
            {
                return (null, null);
            }

            if (response.IsSuccessStatusCode)
            {
                var task = response.Content.ReadAsStreamAsync();
                task.Wait(ct);
                return (task.Result, null);
            }

            if (response.StatusCode == HttpStatusCode.NotFound)
            {
                return (null, null);
            }

            {
                Task<string> task = response.Content.ReadAsStringAsync();
                task.Wait(ct);
                ErrorInfo error;
                try
                {
                    error = JsonConvert.DeserializeObject<ErrorInfo>(task.Result);
                }
                catch (Exception)
                {
                    error = new ErrorInfo(task.Result);
                }

                return (null, error);
            }
        }

        private static HttpClient CreateClient(string host)
        {
            var handler = new HttpClientHandler
            {
                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
            };
            var client = new HttpClient(handler);
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            client.BaseAddress = new Uri(host, UriKind.Absolute);
            return client;
        }

        public DataCenterClient(DataCenterInfo info, bool enableCache = false, string? dataCachePath= null)
        {
            _enableCache = enableCache;
            _dataCachePath = dataCachePath ?? string.Empty;
            _client = CreateClient(info.Host!);
            _tickClient = CreateClient(info.TickFileHost!);
        }

        private string GetDataType(string type)
        {
            return type == "FutureOption" ? "Option" : type;
        }

        public (BarDataField[]?, ErrorInfo?) GetMainDayBars(string symbol, DateTime date1, DateTime date2, CancellationToken ct)
        {
            var url = $"get_main_day_bars?order_book_id={symbol}&start_date={date1:yyyyMMdd}&end_date={date2:yyyyMMdd}";
            var task = _client!.GetAsync(url, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                var (list, error) = ParseJsonResponse<List<string[]>?>(task.Result, ct);
                return list != null ? (ToBarData(list), error) : (null, error);
            }
            return (null, null);
        }

        public (BarDataField[]?, ErrorInfo?) GetDayBars(string symbol, DateTime date1, DateTime date2, CancellationToken ct)
        {
            var url = symbol == "*"
                ? $"future_kline_day?date={date1:yyyyMMdd}"
                : $"future_kline_day?order_book_id={symbol}&start_date={date1:yyyyMMdd}&end_date={date2:yyyyMMdd}";

            var task = _client!.GetAsync(url, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                var (list, error) = ParseJsonResponse<List<string[]>?>(task.Result, ct);
                return list != null ? (ToBarData(list), error) : (null, error);
            }
            return (null, null);
        }

        public (DepthMarketDataField[]?, ErrorInfo?) GetFuturesRealtime(string symbol, CancellationToken ct)
        {
            var task = _client!.GetAsync($"future_tick?order_book_id={symbol}", ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                var (data, error) = ParseFileResponse(task.Result, ct);
                return data != null ? (ProcessTickFile(symbol, DataCenterConst.Future, data, symbol + ".h5"), error) : (null, error);
            }
            return (null, null);
        }

        private DepthMarketDataField[]? ProcessTickFile(string symbol, string type, Stream? stream, string filename)
        {
            var tickFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
            if (_enableCache)
            {
                tickFile = Path.Combine(_dataCachePath, filename);
            }
            if (!File.Exists(tickFile))
            {
                if (stream == null)
                {
                    return null;
                }
                var data = new byte[stream.Length];
                stream.Read(data, 0, (int)stream.Length);
                File.WriteAllBytes(tickFile, data);
            }
            try
            {
                if (type == DataCenterConst.Future)
                {
                    if (HdfReader.IsTickH5(tickFile))
                    {
                        return TickH5.AccumulatedCorrection(TickH5.Load(tickFile));
                    }
                    return HdfReader.Read(tickFile, symbol);
                }
                else if (type == DataCenterConst.FutureOption && HdfReader.IsTickH5(tickFile))
                {
                    return TickH5.AccumulatedCorrection(TickH5.Load(tickFile));
                }
                return HdfXmlReader.Read(tickFile, type);
            }
            finally
            {
                if (!_enableCache)
                {
                    File.Delete(tickFile);
                }
            }
        }

        private (Stream?, string?, ErrorInfo?) GetTickStream(string symbol, string type, DateTime date, CancellationToken ct)
        {
            var dateStr = date.ToString("yyyyMMdd");
            var filename = $"{symbol}_{dateStr}.h5";
            if (_enableCache && File.Exists(Path.Combine(_dataCachePath, filename)))
            {
                return (null, filename, null);
            }
            var url = $"get_tick_files?order_book_id={symbol}&mtype={GetDataType(type)}&start_date={dateStr}&end_date={dateStr}&ret_server_path=0";
            var task = _tickClient!.GetAsync(url, ct);
            task.Wait(ct);
            var (rsp, error) = ParseStringResponse(task.Result, ct);
            if (error == null)
            {
                var token = JToken.Parse(rsp);
                var downloadUrl = token[dateStr]?.Value<string>();
                if (downloadUrl == null)
                {
                    return (null, filename, null);
                }
                task = _tickClient!.GetAsync(new Uri(downloadUrl).PathAndQuery, ct);
                task.Wait(ct);
                if (task.IsCompleted)
                {
                    Stream? stream;
                    (stream, error) = ParseFileResponse(task.Result, ct);
                    return (stream, filename, error);
                }
            }
            return (null, null, error);
        }

        public (BarDataField[]? bars, ErrorInfo? error) GetMinBar(string symbol, string type, DateTime date, CancellationToken ct)
        {
            var (files, error) = GetMinuteBarFiles(symbol, type, date, date, ct);
            if (error != null)
            {
                return (null, error);
            }
            if (files != null && files.Count > 0)
            {
                return GetMinBar(files[0], ct);
            }
            return (null, error);
        }

        public (BarDataField[]? bars, ErrorInfo? error) GetMinBar(string url, CancellationToken ct)
        {
            var task = _client!.GetAsync(url, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                var (stream, error) = ParseFileResponse(task.Result, ct);
                if (stream == null)
                {
                    return (null, error);
                }
                var barFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
                var data = new byte[stream.Length];
                stream.Read(data, 0, (int)stream.Length);
                File.WriteAllBytes(barFile, data);
                var bar = BarH5.Load(barFile);
                File.Delete(barFile);
                return (bar, null);
            }
            return (null, null);
        }

        public (IList<string>? files, ErrorInfo? error) GetMinuteBarFiles(string symbol, string type, DateTime dateTime1, DateTime dateTime2, CancellationToken ct)
        {
            var url = $"get_min_bar_files?order_book_id={symbol}&mtype={GetDataType(type)}&start_date={dateTime1:yyyyMMdd}&end_date={dateTime2:yyyyMMdd}&ret_server_path=0";
            var task = _client!.GetAsync(url, ct);
            task.Wait(ct);
            var (rsp, error) = ParseStringResponse(task.Result, ct);
            if (error == null)
            {
                var token = JObject.Parse(rsp).Properties();
                var list = new List<string>();
                foreach (var item in token)
                {
                    list.Add(item.Value.ToString());
                }
                return (list, null);
            }
            return (null, error);
        }

        public (DepthMarketDataField[]?, ErrorInfo?) GetTicks(string symbol, string type, DateTime date, CancellationToken ct)
        {
            var (stream, filename, error) = GetTickStream(symbol, type, date, ct);
            if (error != null)
            {
                return (null, error);
            }
            var ticks = ProcessTickFile(symbol, type, stream, filename!);
            return (ticks, null);
        }

        private static (InstrumentData[]?, ErrorInfo?) ParseFutures(
            HttpResponseMessage? response,
            InstrumentData[] items,
            CancellationToken ct)
        {
            if (response == null)
            {
                return (null, null);
            }
            var task = response.Content.ReadAsStringAsync();
            task.Wait(ct);
            if (!response.IsSuccessStatusCode)
            {
                return (null, JsonConvert.DeserializeObject<ErrorInfo>(task.Result));
            }
            var data = JToken.Parse(task.Result);
            if (data.First == null)
            {
                return (items, null);
            }
            var dict = items.ToDictionary(item => item.Id);
            var current = data.First;
            while (current != null)
            {
                if (dict.TryGetValue(current["instrument"]!.TryGetValue<string>(), out var item))
                {
                    Helper.ReadFuture(current, item);
                }
                current = current.Next;
            }
            return (items, null);
        }

        private static (InstrumentData[]?, ErrorInfo?) ParseInstrument(
            HttpResponseMessage? response,
            string type,
            CancellationToken ct)
        {
            if (response == null)
            {
                return (null, null);
            }
            var task = response.Content.ReadAsStringAsync();
            task.Wait(ct);
            if (!response.IsSuccessStatusCode)
            {
                return (null, JsonConvert.DeserializeObject<ErrorInfo>(task.Result));
            }
            var data = JToken.Parse(task.Result);
            if (data.First == null)
            {
                return (new InstrumentData[0], null);
            }
            var list = new List<InstrumentData>();
            Func<JToken, InstrumentData?> convert = Helper.ReadStock;
            if (type == DataCenterConst.Option || type == DataCenterConst.FutureOption)
            {
                convert = Helper.ReadOption;
            }
            else if (type == DataCenterConst.Future)
            {
                convert = Helper.ReadFuture;
            }

            var current = data.First;
            while (current != null)
            {
                var item = convert(current);
                if (item != null)
                {
                    list.Add(item);
                }
                current = current.Next;
            }
            return (list.ToArray(), null);
        }

        private (InstrumentData[]?, ErrorInfo?) GetInstrumentData(string[] symbols, string type, CancellationToken ct)
        {
            string GetInstrumentQueryAction(string data)
            {
                switch (data)
                {
                    case DataCenterConst.Option:
                    case DataCenterConst.FutureOption:
                        return "options";
                    case DataCenterConst.Future:
                        return "futures";
                    default:
                        return "instruments";
                }
            }

            var action = GetInstrumentQueryAction(type);
            var task = _client!.GetAsync($"{action}?order_book_id={string.Join(",", symbols)}", ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                return ParseInstrument(task.Result, type, ct);
            }
            return (null, null);
        }

        public (InstrumentData[]?, ErrorInfo?) GetContinuousInstruments(string[] symbols, CancellationToken ct)
        {
            var task = _client!.GetAsync($"instruments?order_book_id={string.Join(",", symbols)}", ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                return ParseInstrument(task.Result, DataCenterConst.Future, ct);
            }
            return (null, null);
        }

        public (InstrumentData[]?, ErrorInfo?) GetInstruments(string[] symbols, string type, CancellationToken ct)
        {
            switch (type)
            {
                case DataCenterConst.Future:
                    return GetFutureData(symbols, type, ct);
                default:
                    return GetInstrumentData(symbols, type, ct);
            }
        }

        private (InstrumentData[]?, ErrorInfo?) GetFutureData(string[] symbols, string type, CancellationToken ct)
        {
            var (items, error) = GetInstrumentData(symbols, type, ct);
            if (error != null)
            {
                return (items, error);
            }
            var task = _client!.GetAsync($"futures?order_book_id={string.Join(",", symbols)}", ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                return ParseFutures(task.Result, items!, ct);
            }
            return (items, null);
        }

        public (string[]?, ErrorInfo?) GetInstrumentNames(string type, DateTime date, CancellationToken ct)
        {
            var args = string.Empty;
            if (!string.IsNullOrEmpty(type))
            {
                args += args.Length == 0 ? "?" : "&";
                args += $"type={GetDataType(type)}";
            }
            if (date != DateTime.MaxValue)
            {
                args += args.Length == 0 ? "?" : "&";
                args += $"date={date:yyyyMMdd}";
            }
            args = $"all_instruments{args}";
            var task = _client!.GetAsync(args, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                return ParseJsonResponse<string[]>(task.Result, ct);
            }
            return (null, null);
        }

        public (string[]?, ErrorInfo?) GetDataFilePath(string symbol, string type, DateTime date1, DateTime date2, CancellationToken ct)
        {
            var args = $"get_tick_files?order_book_id={symbol}&mtype={type}&start_date={date1:yyyyMMdd}&end_date={date2:yyyyMMdd}&ret_server_path=1";
            var task = _client!.GetAsync(args, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                var (data, error) = ParseJsonResponse<Dictionary<string, string>?>(task.Result, ct);
                if (data != null)
                {
                    return (data.Values.ToArray(), error);
                }
                return (null, error);
            }
            return (null, null);
        }

        public (string[]?, ErrorInfo?) GetTradingDays(DateTime date1, DateTime date2, CancellationToken ct)
        {
            var args = $"trading_dates?start_date={date1:yyyyMMdd}&end_date={date2:yyyyMMdd}";
            var task = _client!.GetAsync(args, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                return ParseJsonResponse<string[]>(task.Result, ct);
            }
            return (null, null);
        }

        private static DateTime GetDateTime(string value)
        {
            if (DateTime.TryParseExact(value, "yyyyMMdd", null, DateTimeStyles.None, out var date))
            {
                return date;
            }
            return DateTime.Today;
        }

        private MainFactorInfo[] LoadFactorInfo(object[][] items)
        {
            var list = new MainFactorInfo[items.Length];
            for (int i = 0; i < items.Length; i++)
            {
                var item = items[i];
                var info = new MainFactorInfo();
                info.Date = GetDateTime((string)item[0]);
                info.Symbol = (string)item[1];
                info.AddFactor = (double)item[2];
                info.DivFactor = (double)item[3];
                list[i] = info;
            }
            return list;
        }

        public (MainFactorInfo[]? items, ErrorInfo? error) GetFactorInfo(string symbol, DateTime date1, DateTime date2, CancellationToken ct)
        {
            var args = $"future_main_instrument_target_info?symbol={symbol}&start_date={date1:yyyyMMdd}&end_date={date2:yyyyMMdd}";
            var task = _client!.GetAsync(args, ct);
            task.Wait(ct);
            if (task.IsCompleted)
            {
                var (data, error) = ParseJsonResponse<object[][]?>(task.Result, ct);
                if (data != null && data.Length > 0 && data[0].Length > 2)
                {
                    return (LoadFactorInfo(data), error);
                }
            }
            return (null, null);
        }
    }
}
